/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2011, Salzburg Research. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * sschaffe
 *
 * File Info:
 * User: sschaffe
 * Date: 2011/01/03
 * Time: 15:41
 *
 * Project: KiWi2, kiwi-core module
 */

package kiwi.core.services.query;

import kiwi.core.api.query.SparqlService;
import kiwi.core.api.transaction.TransactionService;
import kiwi.core.api.triplestore.ResourceService;
import kiwi.core.model.rdf.KiWiUriResource;
import org.openrdf.model.impl.URIImpl;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.TupleQuery;
import org.openrdf.query.resultio.TupleQueryResultWriter;
import org.openrdf.query.resultio.sparqljson.SPARQLResultsJSONWriter;
import org.openrdf.query.resultio.sparqlxml.SPARQLResultsXMLWriter;
import org.openrdf.repository.Repository;
import org.openrdf.repository.RepositoryConnection;
import org.slf4j.Logger;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;


/**
 * Add file description here!
 * <p/>
 * User: sschaffe
 */
@Named("kiwi.core.query.sparqlService")
@ApplicationScoped
public class SparqlServiceImpl implements SparqlService {

    /**
     * Get the seam logger for issuing logging statements.
     */
    @Inject
    private Logger log;

    @Inject
    private Repository repository;

    @Inject
    private ResourceService resourceService;

    @Inject
    private TransactionService transactionService;

    // Collection of search signal words
    private String[] WHEREANDMODIFIERCLAUSEL = new String[]{"WHERE", "ORDER", "{", "LIMIT", "OFFSET"};
    private String[] FROMCLAUSEL = new String[]{"FROM"};
    private String[] FROMNAMEDCLAUSEL = new String[]{"NAMED"};
    private String[] BASECLAUSEL = new String[]{"BASE"};
    private String[] PREFIXCLAUSEL = new String[]{"PREFIX"};

	public String query(QueryLanguage queryLanguage, String query, String outputFormat) throws Exception {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		query(queryLanguage, query, out, outputFormat);
		return new String(out.toByteArray(), "utf-8");
	}

	public void query(QueryLanguage queryLanguage, String query, OutputStream os, String outputFormat) throws Exception {
			//build outputwriter
			TupleQueryResultWriter out = null;
			//TODO should be configurable by propterty file
			if(outputFormat == null) {
				out = new SPARQLResultsJSONWriter(os);
			} else if(outputFormat.equals("json")) {
				out = new SPARQLResultsJSONWriter(os);
			} else if(outputFormat.equals("xml")) {
			   out = new SPARQLResultsXMLWriter(os);
			} else throw new Exception("could not produce format "+outputFormat);

			RepositoryConnection connection = repository.getConnection();
			TupleQuery tupleQuery = connection.prepareTupleQuery(queryLanguage,query);
			tupleQuery.evaluate(out);
			connection.close();
			log.info("executed sparql query:{}",query);
	}

    /**
     * It will make the same query like the normal query method but will make the query just for the given
     * knowledgespaces. If someone make a query which include FROM or FROM NAMED clausel it will make a check for the
     * given IRIs. If knowledgeSpaces not include the given IRIs it will return a illegalArgumentException.
     *
     * @param queryLanguage
     * @param query
     * @param outputFormat
     * @param knowledgeSpaces
     * @throws Exception
     */
	public String query(QueryLanguage queryLanguage, String query, String outputFormat, List<KiWiUriResource> knowledgeSpaces) throws Exception {

        log.warn(queryWithSpaces(query,knowledgeSpaces));
		return query(queryLanguage, queryWithSpaces(query,knowledgeSpaces), outputFormat);
	}

    /**
     * It will make the same query like the normal query method but will make the query just for the given
     * knowledgespaces. If someone make a query which include FROM or FROM NAMED clausel it will make a check for the
     * given IRIs. If knowledgeSpaces not include the given IRIs it will return a illegalArgumentException.
     *
     * @param queryLanguage
     * @param query
     * @param os
     * @param outputFormat
     * @param knowledgeSpaces
     * @throws Exception
     */
	public void query(QueryLanguage queryLanguage, String query, OutputStream os, String outputFormat, List<KiWiUriResource> knowledgeSpaces) throws Exception {

        query(queryLanguage, queryWithSpaces(query,knowledgeSpaces), os, outputFormat);
	}

    private String queryWithSpaces(String query, List<KiWiUriResource> knowledgeSpaces) {

        List<String> activeSpaces = makeURIStrings(knowledgeSpaces);

        // make whitespaces for "{}" and "<>"
        query = makeWhitespacesForSpezificCharacter(query);

        // Stringtokenizer with seperator for a exactly copy of the query
        StringTokenizer st = new StringTokenizer(query," \t\n\r\f", true);

        String queryKind = null;
        StringBuilder resultQuery = new StringBuilder("");
        boolean resultBol = false;

        List<String> fromCollector = new ArrayList();
        List<String> fromNamedCollector = new ArrayList();
        String baseuri = null;

        // current position of the next PREFIX or BASE definition in the query
        int basePosition = -1;
        int prefixPosition = -1;

        HashMap<String,String> prefix = new HashMap();
        PrefixDefinition currentPrefix = null;

        boolean constructAfter = false;

        // current position of the next FROM and FROM NAMED clausel in the query
        int positionFrom = -1;
        int positionNamed = -1;


        while(st.hasMoreTokens()) {
            String next = st.nextToken();

            // if next a seperator is it will only add to the result string
            if(!(next.length() == 1 && (next.charAt(0) == ' ' ||
                    next.charAt(0) == '\t' ||
                    next.charAt(0) == '\n' ||
                    next.charAt(0) == '\r' ||
                    next.charAt(0) == '\f'))) {



                // if we have a result until the WHERE,LIMIT etc. we dont must go throug
                // this "if" section
                if(!resultBol) {
                    queryKind = queryKind == null ? getQueryType(next) : queryKind;

                    // Find position of the BASE Clausel and get the IRI
                    basePosition = basePosition == -1 ? hasSignalWord(next, BASECLAUSEL, 1) : basePosition;
                    if(basePosition != -1) {
                        baseuri = getIriRef(next, basePosition, baseuri);
                        basePosition = 0;
                        if(baseuri != null) basePosition = -1;
                    }

                    // Find the position of the prefix
                    if(prefixPosition != -1 && currentPrefix == null) {
                        currentPrefix = getPrefix(next);
                        prefixPosition = 0;
                        if(currentPrefix != null) prefixPosition = -1;
                    } else if(currentPrefix != null) {

                        currentPrefix.setUri(getIriRef(next,0,baseuri));
                        prefix.put(currentPrefix.prefix, currentPrefix.uri);
                        currentPrefix = null;
                    }
                    // A prefix is seperated from the next term EVERYTIME so you can make it just for position 1
                    prefixPosition = prefixPosition == -1 ? hasSignalWord(next, PREFIXCLAUSEL, 1) : prefixPosition;
                }


                // if the queryKind (SELECT,ASK...) is known it begins the FROM or WHERE or in some cases LIMIT block
                if(!resultBol && queryKind != null) {


                    int stepFrom = positionFrom == -1 ? 1 : 0;
                    int stepNamed = positionNamed == -1 ? 1 : 0;
                    String iriRef = null;
                    PrefixDefinition preRef = null;

                    do {

                        // we have a new FROM or FROM NAMED clausel found
                        positionFrom = positionFrom == -1 ? hasSignalWord(next, FROMCLAUSEL, stepFrom) : positionFrom;
                        positionNamed = positionNamed == -1 ? hasSignalWord(next, FROMNAMEDCLAUSEL, stepNamed) : positionFrom;

                        int nextFromPos = hasSignalWord(next, FROMCLAUSEL, stepFrom + 1);
                        if(nextFromPos < positionNamed && nextFromPos >= 0) {
                            positionNamed = -1;
                        }

                        // it is a IRI or are Prefix
                        iriRef = getIriRef(next, positionFrom, baseuri);
                        preRef = getPrefix(next);

                        if(positionFrom != -1) positionFrom = 0;
                        stepFrom++;
                        if(positionNamed != -1) stepNamed++;

                        // ADD the from or From Named Clausel from IRIref or with a PREFIX
                        if(iriRef != null) {

                            if(positionNamed != -1) {
                                fromNamedCollector.add(iriRef);
                            } else {
                                fromCollector.add(iriRef);
                            }

                            positionFrom = -1;
                            positionNamed = -1;
                        } else if(preRef != null) {
                            if(positionNamed != -1) {
                                fromNamedCollector.add(prefix.get(preRef.prefix) + preRef.uri);
                            } else {
                                fromCollector.add(prefix.get(preRef.prefix) + preRef.uri);
                            }

                            positionFrom = -1;
                            positionNamed = -1;
                        }



                    } while(iriRef != null);


                    // If we found the WHERE or MODIFIERCLAUSEL we can make our FROM clausel and close the edit with resultBol = true
                    if((queryKind.equals("SELECT") || queryKind.equals("ASK") || queryKind.equals("DESCRIBE") || constructAfter)) {
                        int position = hasSignalWord(next, WHEREANDMODIFIERCLAUSEL, 1);

                        if(position != -1) {
                            next = next.substring(0, position) + spacesToFromSPARQL(activeSpaces, fromCollector, fromNamedCollector) + next.substring(position);
                            resultBol = true;
                        }
                    } else if(queryKind.equals("CONSTRUCT")) {
                        int position = bracketClosePosition(query) + 1;

                        if(position != -1) {
                            st = new StringTokenizer(query.substring(position + 1)," \t\n\r\f", true);
                            resultQuery = new StringBuilder(query.substring(0,position));
                            next = "";
                            constructAfter = true;
                        }
                    }

                }
            }

            resultQuery.append(next);
        }

        // special case, describe do not need a WHERE or MODIFIER block after the Dataset definition
        if(queryKind.equals("DESCRIBE") && !resultBol) {
            resultQuery = new StringBuilder(query + spacesToFromSPARQL(activeSpaces, fromCollector, fromNamedCollector));
        }

        return resultQuery.toString();

    }

    private List<String> makeURIStrings(List<KiWiUriResource> knowledgeSpaces) {

        List<String> resultSpaces = new ArrayList<String>();

        for(KiWiUriResource resource : knowledgeSpaces) {
            resultSpaces.add(resource.getUri());
        }

        return resultSpaces;

    }

    /**
     * if fromcollector empty it will make FROM clausel for every activeSpaces
     * if fromNamedCollector empty it will make FROM clausel for every activeSpaces
     *
     * if any collector not empty it will only prove the activeSpaces again the collector
     * if the algo find no match it will give a ILLEGALARGUMENTEXCEPTION
     *
     *
     * @param activeSpaces A list of all active spaces as STring
     * @param fromCollector A list of all spaces in the FROM clausels in the query
     * @param fromNamedCollector A list of all spaces in the FROM NAMED clausels in the query
     * @return A List of new FROM and FROM NAMED Clausel
     */
    private String spacesToFromSPARQL(List<String> activeSpaces, List<String> fromCollector, List<String> fromNamedCollector) {

        String fromString = "";

        if(fromCollector.isEmpty()) {
            for(String space : activeSpaces) {
                fromString += " FROM <" + space + "> ";
            }
        } else {
            for(String space : fromCollector) {
                if(!activeSpaces.contains(space)) {
                    throw new IllegalArgumentException("FROM include invalid dataset " + space);
                }
            }
        }

        if(fromNamedCollector.isEmpty()) {
            for(String space : activeSpaces) {
                fromString += " FROM NAMED <" + space + "> ";
            }
        } else {
            for(String space : fromNamedCollector) {
                if(!activeSpaces.contains(space)) {
                    throw new IllegalArgumentException("FROM NAMED include invalid dataset " + space);
                }
            }
        }

        return fromString;
    }

    /**
     * Get the first IRIRef (e.g. <http://www.lala.com/>) in the given String
     * Begin in the STring is the given pos. if a IRI found without a typical
     * IRI form, it will use the baseuri to create the uri.
     *
     * @param search
     * @param pos
     * @param baseuri
     * @return
     */
    private String getIriRef(String search, int pos, String baseuri) {
        if(pos < 0) return null;

        int linkZone = 0;

        StringBuilder builder = new StringBuilder("");

        for(int i = pos; i < search.length();i++) {

            if(linkZone == 0 && search.charAt(i) == '<') {
                linkZone++;
            } else if(linkZone == 1 && search.charAt(i) == '>') {
                linkZone--;

                // is the IRI a uri? if not use the baseuri
                try {
                    URIImpl result = new URIImpl(builder.toString());
                } catch (Exception e) {
                    if(baseuri == null) return builder.toString();
                    return baseuri + builder.toString();
                }

                return builder.toString();
            } else if(linkZone == 1) {
                builder.append(search.charAt(i));
            }
        }

        return null;
    }

    /**
     * Get the prefix of the given String
     *
     * @param search
     * @return
     */
    private PrefixDefinition getPrefix(String search) {

        int prefixZone = 0;
        int linkZone = 0;

        StringBuilder builderPrefix = new StringBuilder("");
        StringBuilder builderUri = new StringBuilder("");

        for(int i = 0; i < search.length();i++) {

            // in <> is it not valid
            if(linkZone == 0 && search.charAt(i) == '<') {
                linkZone++;
            } else if(linkZone == 1 && search.charAt(i) == '>') {
                linkZone--;
            } else if(linkZone == 0) {
                if(prefixZone == 0 && search.charAt(i) == ':') {
                    prefixZone++;
                } else if(prefixZone == 1) {
                    builderUri.append(search.charAt(i));
                } else {
                    builderPrefix.append(search.charAt(i));
                }
            }


        }

        // if nothing found it will get null
        if(prefixZone != 1) return null;

        return new PrefixDefinition(builderPrefix.toString(), builderUri.toString());
    }

    /**
     * Get the first position of a closed bracket "}" of a bracket block "{}"
     * in the given String
     *
     * @param var
     * @return
     */
    private int bracketClosePosition(String var) {

        int linkZone = 0;
        int bracketZone = 0;
        boolean stringZone = false;
        char stringSymbol = (char) 0;

        for(int i = 0; i < var.length();i++) {

            // "" is a String definition and a invalid Zone
            if(!stringZone && (var.charAt(i) == '\"' || var.charAt(i) == '\'')) {
                stringSymbol = var.charAt(i);
                stringZone = true;
            } else if(stringZone && (var.charAt(i) == stringSymbol)) {

                // For: e.g: "bla bla\"" or 'fdf\'dfdf'
                if(i != 0 && !(var.charAt(i - 1) == '\\')) {
                    stringZone = false;
                }
            }

            if(!stringZone) {
                if(linkZone == 0 && var.charAt(i) == '<') {
                    linkZone++;
                } else if(linkZone == 1 && var.charAt(i) == '>') {
                    linkZone--;
                }

                if(linkZone == 0 && var.charAt(i) == '{') {
                    bracketZone++;
                } else if(linkZone == 0 && bracketZone == 1 && var.charAt(i) == '}') {
                    return i;
                } else if(bracketZone > 1 && var.charAt(i) == '}') {
                    bracketZone--;
                }
            }
        }
        return -1;
    }

    /**
     * Look for the "targetNumber" position of one of the given signalwords in
     * the queryPart. "<>", "{}", "?", "$", ":" are invalid signals.
     *
     * @param queryPart
     * @param signalwords
     * @param targetNumber
     * @return
     */
    private int hasSignalWord(String queryPart, String[] signalwords, int targetNumber) {
        int linkZone = 0;
        int currentFav = -1;
        int currentTarget = 0;

        for(int i = 0; i < queryPart.length();i++) {

            if(linkZone == 0 && queryPart.charAt(i) == '<') {
                linkZone++;
            } else if(linkZone == 1 && queryPart.charAt(i) == '>') {
                linkZone--;
            } else {

                if(linkZone == 0) {
                    if(queryPart.charAt(i) == '?') {
                        return currentFav;
                    } else if(queryPart.charAt(i) == '$') {
                        return currentFav;
                    } else if(queryPart.charAt(i) == ':') {
                        return -1;
                    }

                    // is one of the signalwords given in the subquery
                    for(String signalword : signalwords) {
                        try {
                        if(i >= signalword.length() - 1
                                && queryPart.substring(i - (signalword.length() - 1), i + 1).toUpperCase().contains(signalword)
                                && currentFav == -1) {

                            currentTarget++;

                            if(targetNumber == currentTarget) {
                                currentFav = i - (signalword.length() -1);
                                break;
                            }
                        }
                        } catch (Exception e) {
                            int t = 0;
                        }
                    }
                }
            }
        }
        return currentFav;
    }




    /**
     * Make Whitespaces for "{}" blocks and "<>" blocks. e.g:
     * From: "PREFIX foaf:<http://xmlns.com/foaf/0.1/>DESCRIBE ?x FROM foaf:test2 WHERE{ ?x foaf:mbox  ?g  }"
     * To: "PREFIX foaf: <http://xmlns.com/foaf/0.1/> DESCRIBE ?x FROM foaf:test2 WHERE { ?x foaf:mbox  ?g  } "
     * @param query
     * @return
     */
    private static String makeWhitespacesForSpezificCharacter(String query) {

        int linkZone = 0;
        int bracketZone = 0;
        StringBuilder backResult = new StringBuilder("");

        boolean stringZone = false;
        char stringSymbol = (char) 0;

        for(int i = 0; i < query.length();i++) {

            if(!stringZone && (query.charAt(i) == '\"' || query.charAt(i) == '\'')) {
                stringSymbol = query.charAt(i);
                stringZone = true;
            } else if(stringZone && (query.charAt(i) == stringSymbol)) {

                if(i != 0 && !(query.charAt(i - 1) == '\\')) {
                    stringZone = false;
                }
            }

            if(!stringZone) {
                if(linkZone == 0 && query.charAt(i) == '<') {
                    linkZone++;
                    backResult.append(query.substring(0, i)).append(" ");
                    query = query.substring(i);
                    i = 0;
                    continue;
                } else if(linkZone == 1 && query.charAt(i) == '>') {
                    linkZone--;
                    backResult.append(query.substring(0, i + 1)).append(" ");
                    query = query.substring(i+ 1);
                    i = 0;
                    continue;
                }

                if(bracketZone >= 1) {
                    int t = 0;
                }

                if(linkZone == 0 && query.charAt(i) == '{') {
                    bracketZone++;
                    backResult.append(query.substring(0, i)).append(" ");
                    query = query.substring(i);
                    i = 0;
                } else if(linkZone == 0 && bracketZone == 1 && query.charAt(i) == '}') {
                    bracketZone--;
                    backResult.append(query.substring(0, i+ 1)).append(" ");
                    query = query.substring(i +1);
                    i = 0;
                } else if(bracketZone > 1 && query.charAt(i) == '}') {
                    bracketZone--;
                }
            }

        }
        return backResult.append(query).toString();

    }

    /**
     * Get the type of a query a type is "SELECT", "CONSTRUCT", "DESCRIBE" or "ASK"
     * invalid zones are ":" for prefix definition "?" and "$" for a variable or "<>"
     *
     * "{}" is not neccesary, it will not use befor the signal word used
     *
     * @param var
     * @return
     */
    private static String getQueryType(String var) {
        int linkZone = 0;
        String currentFav = null;

        for(int i = 0; i < var.length();i++) {

            if(linkZone == 0 && var.charAt(i) == '<') {
                linkZone++;
            } else if(linkZone == 1 && var.charAt(i) == '>') {
                linkZone--;
            } else {

                if(linkZone == 0) {
                    if(var.charAt(i) == '?') {
                        return currentFav;
                    } else if(var.charAt(i) == '$') {
                        return currentFav;
                    } else if(var.charAt(i) == ':') {
                        return null;
                    }

                    if(i >= 5 && var.substring(i - 5, i + 1).toUpperCase().contains("SELECT") && currentFav == null) {
                        currentFav = "SELECT";
                    } else if(i >= 8 && var.substring(i - 8, i + 1).toUpperCase().contains("CONSTRUCT") && currentFav == null) {
                        currentFav = "CONSTRUCT";
                    } else if(i >= 7 && var.substring(i - 7, i + 1).toUpperCase().contains("DESCRIBE") && currentFav == null) {
                        currentFav = "DESCRIBE";
                    } else if(i >= 2 && var.substring(i - 2, i + 1).toUpperCase().contains("ASK") && currentFav == null) {
                        currentFav = "ASK";
                    }
                }
            }
        }
        return currentFav;
    }

    /**
     * Describe a prefix e.g: "foaf:" with the given uri "<http://www.foaf.com/>"
     *
     */
    static class PrefixDefinition {

        private String prefix;
        private String uri;

        public PrefixDefinition(String prefix, String uri) {
            this.prefix = prefix;
            this.uri = uri;
        }

        /**
         * @return the prefix
         */
        public String getPrefix() {
            return prefix;
        }

        /**
         * @return the uri
         */
        public String getUri() {
            return uri;
        }

        /**
         * @param prefix the prefix to set
         */
        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }

        /**
         * @param uri the uri to set
         */
        public void setUri(String uri) {
            this.uri = uri;
        }

        @Override
        public String toString() {
            return this.getPrefix() + ":" + this.getUri();
        }


    }
}
